1 module unit_threaded.randomized.benchmark;
2 
3 import unit_threaded.from;
4 
5 /* This function used $(D MonoTimeImpl!(ClockType.precise).currTime) to time
6 how long $(D MonoTimeImpl!(ClockType.precise).currTime) takes to return
7 the current time.
8 */
9 private auto medianStopWatchTime() {
10     import core.time;
11     import std.algorithm : sort;
12     import std.datetime : Duration, MonoTimeImpl;
13 
14     enum numRounds = 51;
15     Duration[numRounds] times;
16 
17     MonoTimeImpl!(ClockType.precise) dummy;
18     for (size_t i = 0; i < numRounds; ++i) {
19         auto sw = MonoTimeImpl!(ClockType.precise).currTime;
20         dummy = MonoTimeImpl!(ClockType.precise).currTime;
21         dummy = MonoTimeImpl!(ClockType.precise).currTime;
22         doNotOptimizeAway(dummy);
23         times[i] = MonoTimeImpl!(ClockType.precise).currTime - sw;
24     }
25 
26     sort(times[]);
27 
28     return times[$ / 2].total!"hnsecs";
29 }
30 
31 private from!"std.datetime".Duration getQuantilTick(double q)(
32         from!"std.datetime".Duration[] ticks) pure @safe {
33     size_t idx = cast(size_t)(ticks.length * q);
34 
35     if (ticks.length % 2 == 1) {
36         return ticks[idx];
37     } else {
38         return (ticks[idx] + ticks[idx - 1]) / 2;
39     }
40 }
41 
42 // @Name("Quantil calculations")
43 // unittest
44 // {
45 //     static import std.conv;
46 //     import std.algorithm.iteration : map;
47 
48 //     auto ticks = [1, 2, 3, 4, 5].map!(a => dur!"seconds"(a)).array;
49 
50 //     Duration q25 = getQuantilTick!0.25(ticks);
51 //     assert(q25 == dur!"seconds"(2), q25.toString());
52 
53 //     Duration q50 = getQuantilTick!0.50(ticks);
54 //     assert(q50 == dur!"seconds"(3), q25.toString());
55 
56 //     Duration q75 = getQuantilTick!0.75(ticks);
57 //     assert(q75 == dur!"seconds"(4), q25.toString());
58 
59 //     q25 = getQuantilTick!0.25(ticks[0 .. 4]);
60 //     assert(q25 == dur!"seconds"(1) + dur!"msecs"(500), q25.toString());
61 
62 //     q50 = getQuantilTick!0.50(ticks[0 .. 4]);
63 //     assert(q50 == dur!"seconds"(2) + dur!"msecs"(500), q25.toString());
64 
65 //     q75 = getQuantilTick!0.75(ticks[0 .. 4]);
66 //     assert(q75 == dur!"seconds"(3) + dur!"msecs"(500), q25.toString());
67 // }
68 
69 /** The options  controlling the behaviour of benchmark. */
70 struct BenchmarkOptions {
71     import std.datetime : Duration, seconds;
72 
73     string funcname; // the name of the function to benchmark
74     string filename; // the name of the file the results will be appended to
75     Duration duration = 1.seconds; // the time after which the function to
76     // benchmark is not executed anymore
77     size_t maxRounds = 10000; // the maximum number of times the function
78     // to benchmark is called
79     int seed = 1337; // the seed to the random number generator
80 
81     this(string funcname) {
82         this.funcname = funcname;
83     }
84 }
85 
86 /** This $(D struct) takes care of the time taking and outputting of the
87 statistics.
88 */
89 struct Benchmark {
90     import std.array : Appender;
91     import std.datetime : Duration, MonoTimeImpl, ClockType;
92 
93     string filename; // where to write the benchmark result to
94     string funcname; // the name of the benchmark
95     size_t rounds; // the number of times the functions is supposed to be
96     //executed
97     string timeScale; // the unit the benchmark is measuring in
98     real medianStopWatch; // the median time it takes to get the clocktime twice
99     bool dontWrite; // if set, no data is written to the the file name "filename"
100     // true if, RndValueGen opApply was interrupt unexpectitally
101     Appender!(Duration[]) ticks; // the stopped times, there will be rounds ticks
102     size_t ticksIndex = 0; // the index into ticks
103     size_t curRound = 0; // the number of rounds run
104     MonoTimeImpl!(ClockType.precise) startTime;
105     Duration timeSpend; // overall time spend running the benchmark function
106 
107     /** The constructor for the $(D Benchmark).
108     Params:
109         funcname = The name of the $(D benchmark) instance. The $(D funcname)
110             will be used to associate the results with the function
111         founds = How many rounds.
112         filename = The $(D filename) will be used as a filename to store the
113             results.
114     */
115     this(in string funcname, in size_t rounds, in string filename) {
116         import std.array : appender;
117 
118         this.filename = filename;
119         this.funcname = funcname;
120         this.rounds = rounds;
121         this.timeScale = "hnsecs";
122         this.ticks = appender!(Duration[])();
123         this.medianStopWatch = medianStopWatchTime();
124     }
125 
126     /** A call to this method will start the time taking process */
127     void start() {
128         this.startTime = MonoTimeImpl!(ClockType.precise).currTime;
129     }
130 
131     /** A call to this method will stop the time taking process, and
132     appends the execution time to the $(D ticks) member.
133     */
134     void stop() {
135         auto end = MonoTimeImpl!(ClockType.precise).currTime;
136         Duration dur = end - this.startTime;
137         this.timeSpend += dur;
138         this.ticks.put(dur);
139         ++this.curRound;
140     }
141 
142     ~this() {
143         import std.stdio : File;
144         import std.datetime : Clock;
145 
146         if (!this.dontWrite && this.ticks.data.length) {
147             import std.algorithm : sort;
148 
149             auto sortedTicks = this.ticks.data;
150             sortedTicks.sort();
151 
152             auto f = File(filename ~ "_bechmark.csv", "a");
153             scope (exit)
154                 f.close();
155 
156             auto q0 = sortedTicks[0].total!("hnsecs")() / cast(double) this.rounds;
157             auto q25 = getQuantilTick!0.25(sortedTicks).total!("hnsecs")() / cast(double) this
158                 .rounds;
159             auto q50 = getQuantilTick!0.50(sortedTicks).total!("hnsecs")() / cast(double) this
160                 .rounds;
161             auto q75 = getQuantilTick!0.75(sortedTicks).total!("hnsecs")() / cast(double) this
162                 .rounds;
163             auto q100 = sortedTicks[$ - 1].total!("hnsecs")() / cast(double) this.rounds;
164 
165             // funcname, the data when the benchmark was created, unit of time,
166             // rounds, medianStopWatch, low, 0.25 quantil, median,
167             // 0.75 quantil, high
168             f.writefln("\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"" ~ ",\"%s\"", this.funcname,
169                     Clock.currTime.toISOExtString(), this.timeScale, this.curRound,
170                     this.medianStopWatch, q0 > this.medianStopWatch ? q0 - this.medianStopWatch : 0,
171                     q25 > this.medianStopWatch ? q25 - this.medianStopWatch : 0, q50 > this.medianStopWatch
172                     ? q50 - this.medianStopWatch : 0, q75 > this.medianStopWatch ? q75 - this.medianStopWatch
173                     : 0, q100 > this.medianStopWatch ? q100 - this.medianStopWatch : 0);
174         }
175     }
176 }
177 
178 void doNotOptimizeAway(T...)(ref T t) {
179     foreach (ref it; t) {
180         doNotOptimizeAwayImpl(&it);
181     }
182 }
183 
184 private void doNotOptimizeAwayImpl(void* p) {
185     import core.thread : getpid;
186     import std.stdio : writeln;
187 
188     if (getpid() == 1) {
189         writeln(*cast(char*) p);
190     }
191 }
192 
193 // unittest
194 // {
195 //     static void funToBenchmark(int a, float b, Gen!(int, -5, 5) c, string d,
196 //         GenASCIIString!(1, 10) e)
197 //     {
198 //         import core.thread;
199 
200 //         Thread.sleep(1.seconds / 100000);
201 //         doNotOptimizeAway(a, b, c, d, e);
202 //     }
203 
204 //     benchmark!funToBenchmark();
205 //     benchmark!funToBenchmark("Another Name");
206 //     benchmark!funToBenchmark("Another Name", 2.seconds);
207 //     benchmark!funToBenchmark(2.seconds);
208 // }
209 
210 /** This function runs the passed callable $(D T) for the duration of
211 $(D maxRuntime). It will count how often $(D T) is run in the duration and
212 how long each run took to complete.
213 
214 Unless compiled in release mode, statistics will be printed to $(D stderr).
215 If compiled in release mode the statistics are appended to a file called
216 $(D name).
217 
218 Params:
219     opts = A $(D BenchmarkOptions) instance that encompasses all possible
220         parameters of benchmark.
221     name = The name of the benchmark. The name is also used as filename to
222         save the benchmark results.
223     maxRuntime = The maximum time the benchmark is executed. The last run will
224         not be interrupted.
225     rndSeed = The seed to the random number generator used to populate the
226         parameter passed to the function to benchmark.
227     rounds = The maximum number of times the callable $(D T) is called.
228 */
229 void benchmark(alias T)(const ref BenchmarkOptions opts) {
230     import std.random : Random;
231     import std.traits : ParameterIdentifierTuple, Parameters;
232     import unit_threaded.randomized.random;
233 
234     auto bench = Benchmark(opts.funcname, opts.maxRounds, opts.filename);
235     auto rnd = Random(opts.seed);
236     enum string[] parameterNames = [ParameterIdentifierTuple!T];
237     auto valueGenerator = RndValueGen!(parameterNames, Parameters!T)(&rnd);
238 
239     while (bench.timeSpend <= opts.duration && bench.curRound < opts.maxRounds) {
240         valueGenerator.genValues();
241 
242         bench.start();
243         try {
244             T(valueGenerator.values);
245         } catch (Throwable t) {
246             import std.experimental.logger : logf;
247 
248             logf("unittest with name %s failed when parameter %s where passed",
249                     opts.funcname, valueGenerator);
250             break;
251         } finally {
252             bench.stop();
253             ++bench.curRound;
254         }
255     }
256 }
257 
258 /// Ditto
259 void benchmark(alias T)(string funcname = "", string filename = __FILE__) {
260     import std..string : empty;
261     import std.traits : fullyQualifiedName;
262 
263     auto opt = BenchmarkOptions(funcname.empty ? fullyQualifiedName!T : funcname);
264     opt.filename = filename;
265     benchmark!(T)(opt);
266 }
267 
268 /// Ditto
269 void benchmark(alias T)(from!"std.datetime".Duration maxRuntime, string filename = __FILE__) {
270     import std.traits : fullyQualifiedName;
271 
272     auto opt = BenchmarkOptions(fullyQualifiedName!T);
273     opt.filename = filename;
274     opt.duration = maxRuntime;
275     benchmark!(T)(opt);
276 }
277 
278 /// Ditto
279 /*void benchmark(alias T)(string name, string filename = __FILE__)
280 {
281     auto opt = BenchmarkOptions(name);
282     opt.filename = filename;
283     benchmark!(T)(opt);
284 }*/
285 
286 /// Ditto
287 void benchmark(alias T)(string name, from!"std.datetime".Duration maxRuntime,
288         string filename = __FILE__) {
289     auto opt = BenchmarkOptions(name);
290     opt.filename = filename;
291     opt.duration = maxRuntime;
292     benchmark!(T)(opt);
293 }